#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <poll.h>
#include <vector>
#include <iostream>
#include "server.h"
#include "util.h"
#include "incoming_connection.h"
#include "outgoing_connection.h"

using namespace std;

Server::Server(int port) : ready(false) {
	if ((socket = ::socket(PF_INET, SOCK_STREAM, 0)) < 0) {
		error("Couldn't obtain server socket");
		return;
	}
	// Si esto falla, mala suerte.
	int reuse = 1;
	setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = htonl(INADDR_ANY);
	addr.sin_port = htons(port);
	if (bind(socket, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
		close(socket);
		error("Couldn't bind socket");
		return;
	}

	if (listen(socket, 5) < 0) {
		close(socket);
		error("Couldn't put socket in listen mode");
		return;
	}
	fds[0].fd = STDIN_FILENO;
	fds[0].events = POLLIN;
	fds[0].revents = 0;
	fds[1].fd = socket;
	fds[1].events = POLLIN;
	fds[1].revents = 1;
	
	ready = true;
}

void Server::run() {
	const int timeout = 5;
	
	while (ready) {
		if (poll(fds, connections.size() + 2, timeout) < 0) {
			error("Quiting...");
			break;
		}

		// Manejar comandos al servidor.
		if (fds[0].revents & POLLIN) {
			parse_input();
		}
		
		// Manejar nuevas conexiones entrantes.
		if (fds[1].revents & POLLIN) {
			handle_new_connection();
		}

		// Atender los mensajes que llegan de los clientes conectados.
		for (size_t i = 0; i < connections.size(); i++) {
			connections[i]->process_events();
			if (connections[i]->is_dead()) {
				// Si una conexión muere, la movemos intercambiándola por
				// la última de nuestra lista, para así poder eliminarla.
				int n = connections.size() - 1;
				swap(fds[i+2], fds[n+2]);
				swap(connections[i], connections[n]);
				connections[i]->set_poll_info(&fds[i+2]);
				// La eliminamos.
				delete connections.back();
				connections.pop_back();
			}
		}
	}
}

FileIndex &Server::get_index() {
	return index;
}

void Server::parse_input() {
	string buf;
	cin >> buf;
	if (buf == "quit") {
		ready = false;
	}
}

void Server::handle_new_connection() {
	struct sockaddr_in addr;
	socklen_t len = sizeof(addr);
	int client_socket = accept(socket, (struct sockaddr *) &addr, &len);
	if (client_socket < 0) {
		error("Couldn't accept incoming connection");
	} else {
		int n = connections.size() + 2;
		if (n < MAX_CLIENTS) {
			IncomingConnection *conn = new IncomingConnection(this,
				client_socket, &fds[n]);
			conn->set_address(string(inet_ntoa(addr.sin_addr)));
			connections.push_back(conn);
		} else {
			log("Maximum number of clients reached. Closing connection.");
			close(client_socket);
		}
	}
}

bool Server::retrieve_file_info(const std::string md5, const Source *source) {
	int n = connections.size() + 2;
	if (!source->is_address_valid()) {
		log("Client address is invalid.");
	} else if (n == MAX_CLIENTS) {
		log("Can't connect to client. Maximum number of clients reached.");
	} else if (outgoing_connections.find(source->get_address()) !=
	           outgoing_connections.end()) {
		outgoing_connections[source->get_address()]->enqueue_file(md5, source);
		return true;
	} else {
		vector<string> dir = parse(source->get_address(), ":");
		
		int client_socket = 0;
		if ((client_socket = ::socket(PF_INET, SOCK_STREAM, 0)) < 0) {
			error("Couldn't obtain socket");
		} else {
			struct sockaddr_in addr;
			memset(&addr, 0, sizeof(addr));
			addr.sin_family = AF_INET;
			addr.sin_port = htons(string_to_int(dir[1]));

			struct hostent *hostinfo;
			if ((hostinfo = gethostbyname(dir[0].c_str())) < 0) {
				// No usamos error porque gethostbyname no reporta sus
				// errores en errno.
				log("Error resolving client address.");
			} else {
				addr.sin_addr = *(struct in_addr *)hostinfo->h_addr;
				if (connect(client_socket, (struct sockaddr *)&addr,
				            sizeof(addr)) < 0) {
					error("Error connecting to client");
				} else {
					OutgoingConnection *conn = new OutgoingConnection(
							this, source->get_address(),
							client_socket, &fds[n]);
					connections.push_back(conn);
					conn->enqueue_file(md5, source);
					return true;
				}
			}
		}
	}
	return false;
}

void Server::add_connection_to(const std::string address,
                               OutgoingConnection* conn) {
	outgoing_connections[address] = conn;
}

void Server::remove_connection_to(const std::string address) {
	outgoing_connections.erase(address);
}

Server::~Server() {
	if (socket >= 0) {
		close(socket);
	}
	for (size_t i = 0; i < connections.size(); i++) {
		delete connections[i];
	}
}

